/*
 * ca.js
 */
(function() {

	function extend(target, object) {
		for (var i in object) {
			target[i] = object[i];
		}
	}
	
	/*
	 * 
	 */
	var BIT = [0, 1, 2, 4, 8, 16, 32, 64, 128];
	var EIGHT_SUM_STATUS = [];
	var FOUR_SUM_STATUS = [];
	(function() {

		function total8(number) {
			return total(number, 8);
		}
			
		function total4(number) {
			return total(number, 4);
		}
	
		function total(number, until) {
			var c = 0;
			for (var i = 1; i <=until; i++) {
				c += (0 < (number & BIT[i])) ? 1 : 0;
			}
			return c;
		}

		for (var i = 0; i < 256; i++) {
			EIGHT_SUM_STATUS[i] = total8(i);
			FOUR_SUM_STATUS[i] = total4(i);
		}
	})();

	/*
	 * Universe
	 */
	function create2dArray(x, y) {
		var a = [];
		for (var i = 0; i < x; i++) {
			a[i] = [];
			for (var j = 0; j < y; j++) {
				 a[i][j] = 0;
			}
		}
		return a;
	}

	/*
	 * 815
	 * 4+2
	 * 736
	 */
	var sum = {
		getNeiborhood: function(x, y, filter) {
			return (filter(this.get(x, y - 1))) |
			((filter(this.get(x + 1, y))) << 1) |
			((filter(this.get(x, y + 1))) << 2) |
			((filter(this.get(x - 1, y ))) << 3) |
			((filter(this.get(x + 1, y - 1))) << 4) |
			((filter(this.get(x + 1, y + 1))) << 5) |
			((filter(this.get(x - 1, y + 1))) << 6) |
			((filter(this.get(x - 1, y - 1))) << 7);
		},
		eightStatus: function(x, y, number) {
			return (number == this.get(x, y - 1)) |
				((number == this.get(x + 1, y)) << 1) |
				((number == this.get(x, y + 1)) << 2) |
				((number == this.get(x - 1, y )) << 3) |
				((number == this.get(x + 1, y - 1)) << 4) |
				((number == this.get(x + 1, y + 1)) << 5) |
				((number == this.get(x - 1, y + 1)) << 6) |
				((number == this.get(x - 1, y - 1)) << 7);
		},
	
		eightStatusWithoutZero: function(x, y) {
			return (0 < this.get(x, y - 1)) |
				((0 < this.get(x + 1, y)) << 1) |
				((0 < this.get(x, y + 1)) << 2) |
				((0 < this.get(x - 1, y )) << 3) |
				((0 < this.get(x + 1, y - 1)) << 4) |
				((0 < this.get(x + 1, y + 1)) << 5) |
				((0 < this.get(x - 1, y + 1)) << 6) |
				((0 < this.get(x - 1, y - 1)) << 7);
		},
	
		eightSum: function(x, y, number) {
			return (number == this.get(x, y - 1)) +
				(number == this.get(x + 1, y - 1)) +
				(number == this.get(x + 1, y)) +
				(number == this.get(x + 1, y + 1)) +
				(number == this.get(x, y + 1)) +
				(number == this.get(x - 1, y + 1)) +
				(number == this.get(x - 1, y)) +
				(number == this.get(x - 1, y - 1));
		},
	
		eightSumWithoutZero: function(x, y) {
			return (0 < this.get(x, y - 1)) +
				(0 < this.get(x + 1, y - 1)) +
				(0 < this.get(x + 1, y)) +
				(0 < this.get(x + 1, y + 1)) +
				(0 < this.get(x, y + 1)) +
				(0 < this.get(x - 1, y + 1)) +
				(0 < this.get(x - 1, y)) +
				(0 < this.get(x - 1, y - 1));
		},
	
		fourSum: function(x, y, number) {
			return (number == this.get(x, y - 1)) +
				(number == this.get(x + 1, y)) +
				(number == this.get(x, y + 1)) +
				(number == this.get(x - 1, y));
		},
	
		fourSumWithoutZero: function(x, y) {
			return (0 < this.get(x, y - 1)) +
				(0 < this.get(x + 1, y)) +
				(0 < this.get(x, y + 1)) +
				(0 < this.get(x - 1, y));
		},
	};

	/*
	 * UniverseOpened
	 */
	function UniverseOpend(x, y) {
		this.sx = x;
		this.sy = y;
		this.universe = create2dArray(x, y);
	}

	UniverseOpend.prototype = {
		isOut: function(x, y) {
			return 0 > x || 0 > y || this.sx <= x || this.sy <= y;
		},
		
		get: function(x, y) {
			if (this.isOut(x, y)) return 0;
			return this.universe[x][y];
		},
		
		set: function(x, y, v) {
			if (!this.isOut(x, y)) this.universe[x][y] = v;
		},

	};
	
	extend(UniverseOpend.prototype, sum);
	
	/*
	 * UniverseClosed
	 */
	function UniverseClosed(x, y) {
		this.sx = x;
		this.sy = y;
		this.universe = create2dArray(x, y);
	}
  
	function loop(number, max) {
		if (0 <= number && number < max) return number;
		if (number >= max) return number % max;
		return max - 1 + ((number + 1) % max);	
	}

	UniverseClosed.prototype = {
		isOut: function(x, y) {
			return false;
		},
		
		get: function(x, y) {
			return this.universe[loop(x, this.sx)][loop(y, this.sy)];
		},
		
		set: function(x, y, v) {
			this.universe[loop(x, this.sx)][loop(y, this.sy)] = v;
		},
	};

	extend(UniverseClosed.prototype, sum);

	/*
	 * GenerationalUniverse
	 */
	function GenerationalUniverse(x, y, closed, rule) {
		this.sx = x;
		this.sy = y;
		var u;
		if (closed) {
			u = [new UniverseClosed(x, y), new UniverseClosed(x, y)];
		} else {
			u = [new UniverseOpend(x, y), new UniverseOpend(x, y)];
		}
		this.universe = u;
		this.now = 0;
		this.targetUniverse = this.universe[this.now];
		this.rule = rule;
	}

	GenerationalUniverse.prototype = {
		isOut: function(x, y) {
			this.targetUniverse.isOut(x, y);
		},
	  
		get: function(x, y) {
			return this.targetUniverse.get(x, y);
		},
	  
		set: function(x, y, v) {
			this.targetUniverse.set(x, y, v);
		},
	  
		next: function(f) {
			var next = f ? f.next : this.rule.next;
			var n = this.universe[this.now];
			var r = this.universe[1 - this.now];
			var sx = this.sx, sy = this.sy;
			for (var x = 0; x < sx; x++) {
				for (var y = 0; y < sy; y++) {
					r.set(x, y, next(x, y, n));
				}
			}
			this.now = 1 - this.now;
			this.targetUniverse = this.universe[this.now];
		},

		getNeiborhood: function(x, y, filter) {
			return this.targetUniverse.getNeiborhood(x, y, filter);
		},

		eightStatus: function(x, y, number) {
			return this.targetUniverse.eightStatus(x, y, number);
		},

		eightStatusWithoutZero: function(x, y) {
			return this.targetUniverse.eightStatusWithoutZero(x, y);
		},

		eightSum: function(x, y, number) {
			return this.targetUniverse.eightSum(x, y, number);
		},

		eightSumWithoutZero: function(x, y) {
			return this.targetUniverse.eightSumWithoutZero(x, y);
		},

		fourSum: function(x, y, number) {
			return this.targetUniverse.fourSum(x, y, number);
		},

		fourSumWithoutZero: function(x, y) {
			return this.targetUniverse.fourSumWithoutZero(x, y);
		},
	};
	
	/*
	 * Rule
	 */
	var RULE_GENERATIONS = new RegExp("([0-9]*)/([0-9]*)(,([0-9]+))*");

	/*
	 * split("123456") -> [1,2,3,4,5,6]
	 */
	function split(value) {
		var a = [];
		for (var i = 0; i < value.length; i++) {
			a[i] = parseInt(value.charAt(i));
		}
		return a;
	}

	/*
	 * state("12356",1) -> [undefined,1,1,1,undefined,1,1]
	 */
	function state(value, status) {
		var r = [];
		var numbers = split(value);
		for (var i in numbers) {
			r[numbers[i]] = status;
		}
		return r;
	}

	function Rule(name, code, status, next) {
		this.name = name;
		this.code = code;
		this.status = status
		this.next = next;
	}

	Rule.table = function(name, code, table) {
		var next = function(x, y, universe) {
			var s = universe.get(x, y);
			var c = universe.eightSum(x, y, 1);
			var v = rule[s];
			if (null != v) {
				v = v[c];
			}
			return v || 0;
		};
		return new Rule("", "", rule.length, calc);
	};

	/*
	 * Format : "B/S,C"
	 *   B : Birth, sets the array of eight summary count
	 *   S : Survive
	 *   C : Count, the count of states cells can have (including 0 state).
	 * ex) 2/23,3
	 */
	Rule.generations = function(name, code) {
		var match = code.match(RULE_GENERATIONS);
		var rule = [];
		// birth
		rule[0] = state(match[1], 1);
		// survive
		rule[1] = state(match[2], 1);
		var next;
		var generations;
		if (match[3]) {
			// generations
			generations = parseInt(match[4]);
			next = function(x, y, universe) {
				var s = universe.get(x, y);
				if (1 < s) {
					return (s < generations - 1) ? s + 1 : 0;
				} else {
					var v = rule[s][universe.eightSum(x, y, 1)] || 0;
					if (1 == s && 0 == v) {
						v = (s < generations - 1) ? 2 : 0;
					}
					return  v;
				}
			};
		} else {
			// life
			generations = 1;
			next = function(x, y, universe) {
				var s = universe.get(x, y);
				return rule[s][universe.eightSum(x, y, 1)] || 0;
			};
		}
		return new Rule(name, code, generations, next);
	};

	Rule.random = function(n) {
		var next = function(x, y, universe) {
			return n > Math.random() ? 1 : 0;
		};
		return new Rule("random", "", 1, next);
	};

	function CanvasDrawer(canvas, universe, color, pix) {
		this.context = canvas.getContext("2d");
		canvas.width = universe.sx * pix;
		canvas.height = universe.sy * pix;
		this.universe = universe;
		this.color = color;
		this.pix = pix;
	}

	CanvasDrawer.prototype = {
		draw: function() {
			var sx = this.universe.sx;
			var sy = this.universe.sy;
			var universe = this.universe;
			var c = this.context;
			var cl = this.color;
			var pix = this.pix;
			c.fillStyle = cl.background();
			c.fillRect(0, 0, sx * pix, sy * pix);
			var lastStatus = 0;
			var gx = 0;
			var gy = 0;
			for (var y = 0; y < sy; y++) {
				gx = 0;
				for (var x = 0; x < sx; x++) {
					var s = cl.get(x, y, universe);
					if (s) {
						if (lastStatus != s) {
							c.fillStyle = s;
							lastStatus = s; 
						}
						c.fillRect(gx, gy, pix, pix);
					}
					gx += pix;
				}
				gy += pix;
			}
		},
	};


	/*
	 * Color package
	 */
	var Color = {};

	/*
	 * RgbColor
	 */
	function RgbColor(r, g, b) {
		this.red = r;
		this.green = g;
		this.blue = b;
	}

	RgbColor.prototype = {
		toString: function() {
			var n = ((parseInt(this.red) & 0xff) << 16) |
				((parseInt(this.green) & 0xff) << 8) |
				(parseInt(this.blue) & 0xff);
			n = n.toString(16);
			return "#" + "000000".substr(0, 6 - n.length) + n;
		},
	
		add: function(rgbColor) {
			return new RgbColor(
				this.red + rgbColor.red,
				this.green + rgbColor.green,
				this.blue + rgbColor.blue);
		},

		sub: function(rgbColor) {
			return new RgbColor(
				this.red - rgbColor.red,
				this.green - rgbColor.green,
				this.blue - rgbColor.blue);
		},

		div: function(number) {
			return new RgbColor(
				this.red / number,
				this.green / number,
				this.blue / number);
		},

		mul: function(number) {
			return new RgbColor(
				this.red * number,
				this.green * number,
				this.blue * number);
		},
	};

	RgbColor.parse = function(color) {
		return new RgbColor(
			(color & 0xff0000) >> 16,
			(color & 0xff00) >> 8,
			(color & 0xff));
	}

	/*
	 * GradationalColor
	 */
	function GradationalColor(backgroundColor, fromColor, toColor) {
		this.backgroundColor = RgbColor.parse(backgroundColor).toString();
		this.fromColor =  RgbColor.parse(fromColor);
		this.toColor =  RgbColor.parse(toColor);
	}

	GradationalColor.prototype = {
		create: function(universe) {
			var colors = [];
			var len = universe.rule.status;
			var c = this.fromColor;
			var s = this.toColor.sub(this.fromColor).div(len);
			for (var i = 1; i <= len; i++) {
				colors[i] = c.toString();
				c = c.add(s);
			}
			return new GradationalColorSelector(this.backgroundColor, colors, universe);
		},
	};
	
	function GradationalColorSelector(backgroundColor, colors, universe) {
		this.backgroundColor = backgroundColor;
		this.colors = colors;
		this.universe = universe;
	}

	GradationalColorSelector.prototype = {
		get: function(x, y) {
			var s = this.universe.get(x, y);
			if (0 == s) return null; 
			return this.colors[s];
		},
	
		background: function() {
			return this.backgroundColor;
		},
	};

	Color.GradationalColor = GradationalColor;

	/*
	 * RepeatedColor 
	 */
	function RepeatedColor(colors) {
		this.colors = colors;
	}

	RepeatedColor.prototype = {
		create: function(universe) {
			return new RepeatedColorSelector(this.colors, universe);
		},
	};

	function RepeatedColorSelector(colors, universe) {
		this.colors = colors;
		this.universe = universe;
	}

	RepeatedColorSelector.prototype = {
		get: function(x, y) {
			var s = this.universe.get(x, y);
			if (0 == s) return null; 
			return this.colors[s] || this.colors[s % (this.colors.length - 1) + 1];
		},

		background: function() {
			return this.colors[0];
		},
	};

	Color.RepeatedColor = RepeatedColor;


	/*
	 * EightSumColor
	 */
	function EightSumColor(colors) {
		this.colors = colors;
	}

	EightSumColor.prototype.create = function(universe) {
		return new EightSumColorSelector(this.colors, universe);
	};
  
	function EightSumColorSelector(colors, universe) {
		this.colors = colors;
		this.universe = universe;
		this.universeColor = new UniverseClosed(universe.sx, universe.sy);
	}

	EightSumColorSelector.prototype = {
		get: function(x, y) {
			var s = this.universe.get(x, y);
			if (0 == s) {
				this.universeColor.set(x, y, 0);
				return null;
			}
			var c = this.universeColor.get(x, y)
			if (0 == c) {
				c = this.universe.eightSumWithoutZero(x, y) + 1;
				this.universeColor.set(x, y, c);
			}
			return this.colors[c];
		},

		background: function() {
			return this.colors[0];
		},
	}

	Color.EightSumColor = EightSumColor;

	/*
	 * EightFourSumColor
	 * the length of colors is 26.
	 * the zero is dead color.
	 */
	function EightFourSumColor(colors) {
		this.colors = colors;
	}

	EightFourSumColor.prototype.create = function(universe) {
		return new EightFourSumColorSelector(this.colors, universe);
	};
  
	function EightFourSumColorSelector(colors, universe) {
		this.colors = colors;
		this.universe = universe;
		this.universeColor = new UniverseClosed(universe.sx, universe.sy);
	}

	EIGHT_FOUR_COLOR_INDEX = [1, 2, 4, 7, 11, 16, 20, 22, 25];
	
	EightFourSumColorSelector.prototype = {
		get: function(x, y) {
			var s = this.universe.get(x, y);
			if (0 == s) {
				this.universeColor.set(x, y, 0);
				return null;
			}
			var c = this.universeColor.get(x, y)
			if (0 == c) {
				var st = this.universe.eightStatusWithoutZero(x, y);
				c = EIGHT_FOUR_COLOR_INDEX[EIGHT_SUM_STATUS[st]] + FOUR_SUM_STATUS[st];
				this.universeColor.set(x, y, c);
			}
			return this.colors[c];
		},

		background: function() {
			return this.colors[0];
		},
	}

	Color.EightFourSumColor = EightFourSumColor;

	/*
	 * object {
	 *     element: window.element
	 *     universe: {x: Int, y: Int, closed: boolean},
	 *     rule: function(x, y, m),
	 *     color: Color,
	 *     pix: Int
	 * }
	 */
	function CellAutomata(object) {
		this.universe = new GenerationalUniverse(object.universe.x, object.universe.y, object.universe.closed, object.rule);
		this.drawer = new CanvasDrawer(object.element, this.universe, object.color.create(this.universe), object.pix);
	}

	CellAutomata.prototype = {
		set: function(rule) {
			this.universe.next(rule);
		},
	
		draw: function() {
			this.drawer.draw();
		},
	
		next: function(rule) {
			this.set(rule);
			this.draw();
		},
	};

	function pack(pack, target){
		var na = pack.split(".");
		var t = window;
		for (var i in na) {
		    t[na[i]] = t[na[i]] || {};
		    t = t[na[i]];
		}
		for (var i in target) {
			t[i] = target[i];
		}
	}

	pack("akinao.ca", {
		Rule: Rule,
		Color: Color,
		CellAutomata: CellAutomata,
	});

})();
